-- -------------------------------------------------------------------------
-- Lib/json.lua
-- -------------------------------------------------------------------------
-- Optimized Pure Lua JSON parser
-- Performance: Local caching, table-based string building, compact output
-- -------------------------------------------------------------------------

local json = { _version = "0.2.0", max_depth = 50 }

-- Local caching of global functions for performance
local type = type
local pairs = pairs
local ipairs = ipairs
local tostring = tostring
local tonumber = tonumber
local next = next
local rawget = rawget
local error = error

local string_char = string.char
local string_byte = string.byte
local string_sub = string.sub
local string_find = string.find
local string_format = string.format
local string_gsub = string.gsub
local string_len = string.len

local table_concat = table.concat
local table_sort = table.sort

local math_floor = math.floor

-- -------------------------------------------------------------------------
-- Encoder
-- -------------------------------------------------------------------------

local encode

local escape_char_map = {
    ["\\"] = "\\\\",
    ["\""] = "\\\"",
    ["\b"] = "\\b",
    ["\f"] = "\\f",
    ["\n"] = "\\n",
    ["\r"] = "\\r",
    ["\t"] = "\\t",
}

local escape_char_map_inv = {
    ["\\"] = "\\",
    ["\""] = "\"",
    ["/"] = "/",
    ["b"] = "\b",
    ["f"] = "\f",
    ["n"] = "\n",
    ["r"] = "\r",
    ["t"] = "\t",
}

local function escape_char(c)
    return escape_char_map[c] or string_format("\\u%04x", string_byte(c))
end

local function encode_nil()
    return "null"
end

local function encode_table(val, stack, depth)
    if depth > json.max_depth then
        error("maximum depth exceeded")
    end

    local res = {}
    stack = stack or {}

    -- Circular reference check
    if stack[val] then error("circular reference") end
    stack[val] = true

    -- Check if array or object
    local is_array = true
    local max_index = 0
    local count = 0

    for k, _ in pairs(val) do
        count = count + 1
        if type(k) == "number" and k > 0 and math_floor(k) == k then
            if k > max_index then max_index = k end
        else
            is_array = false
            break
        end
    end

    -- If it's a sparse array with many holes, treat as object
    if is_array and max_index > count * 2 then
        is_array = false
    end

    if is_array then
        -- Encode as array
        for i = 1, max_index do
            local v = rawget(val, i)
            if v == nil then
                res[#res + 1] = "null"
            else
                res[#res + 1] = encode(v, stack, depth + 1)
            end
        end
        stack[val] = nil
        return "[" .. table_concat(res, ",") .. "]"
    else
        -- Encode as object
        local keys = {}
        for k in pairs(val) do
            if type(k) == "string" then
                keys[#keys + 1] = k
            elseif type(k) == "number" then
                keys[#keys + 1] = tostring(k)
            end
        end
        table_sort(keys)

        for i = 1, #keys do
            local k = keys[i]
            local v = val[k]
            if v == nil then v = val[tonumber(k)] end
            if v ~= nil then
                local encoded_key = '"' .. string_gsub(k, '[%z\1-\31\\"]', escape_char) .. '"'
                res[#res + 1] = encoded_key .. ":" .. encode(v, stack, depth + 1)
            end
        end
        stack[val] = nil
        return "{" .. table_concat(res, ",") .. "}"
    end
end

local function encode_string(val)
    return '"' .. string_gsub(val, '[%z\1-\31\\"]', escape_char) .. '"'
end

local function encode_number(val)
    -- Handle special cases
    if val ~= val then
        error("cannot encode NaN")
    elseif val == math.huge or val == -math.huge then
        error("cannot encode Infinity")
    end

    -- Use integer format when possible
    if math_floor(val) == val and val >= -2147483648 and val <= 2147483647 then
        return string_format("%d", val)
    end

    return string_format("%.14g", val)
end

local type_func_map = {
    ["nil"] = encode_nil,
    ["table"] = encode_table,
    ["string"] = encode_string,
    ["number"] = encode_number,
    ["boolean"] = tostring,
}

encode = function(val, stack, depth)
    local t = type(val)
    local f = type_func_map[t]
    if f then
        return f(val, stack, depth or 0)
    end
    error("unexpected type '" .. t .. "'")
end

function json.encode(val)
    return encode(val)
end

-- -------------------------------------------------------------------------
-- Decoder
-- -------------------------------------------------------------------------

local parse

local function create_set(...)
    local res = {}
    for i = 1, select("#", ...) do
        res[select(i, ...)] = true
    end
    return res
end

local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null")

local literal_map = {
    ["true"] = true,
    ["false"] = false,
    ["null"] = nil,
}

local function next_char(str, idx, set, negate)
    for i = idx, string_len(str) do
        if set[string_sub(str, i, i)] ~= negate then
            return i
        end
    end
    return string_len(str) + 1
end

local function decode_error(str, idx, msg)
    local line_count = 1
    local col_count = 1
    for i = 1, idx - 1 do
        col_count = col_count + 1
        if string_sub(str, i, i) == "\n" then
            line_count = line_count + 1
            col_count = 1
        end
    end
    error(string_format("%s at line %d col %d", msg, line_count, col_count))
end

local function codepoint_to_utf8(n)
    -- http://scripts.sil.org/IWS-Appendix-A
    local f = math_floor
    if n <= 0x7f then
        return string_char(n)
    elseif n <= 0x7ff then
        return string_char(f(n / 64) + 192, n % 64 + 128)
    elseif n <= 0xffff then
        return string_char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
    elseif n <= 0x10ffff then
        return string_char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
            f(n % 4096 / 64) + 128, n % 64 + 128)
    end
    error(string_format("invalid unicode codepoint '%x'", n))
end

local function parse_unicode_escape(s)
    local n1 = tonumber(s:sub(1, 4), 16)
    local n2 = tonumber(s:sub(7, 10), 16)
    -- Surrogate pair?
    if n2 then
        return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
    else
        return codepoint_to_utf8(n1)
    end
end

local function parse_string(str, i)
    local res = {}
    local j = i + 1
    local k = j

    while j <= string_len(str) do
        local x = string_byte(str, j)

        if x < 32 then
            decode_error(str, j, "control character in string")
        elseif x == 92 then -- `\`: Escape
            res[#res + 1] = string_sub(str, k, j - 1)
            j = j + 1
            local c = string_sub(str, j, j)
            if c == "u" then
                local hex = string_sub(str, j + 1, j + 4)
                if not hex:find("^%x%x%x%x") then
                    decode_error(str, j + 1, "invalid unicode escape in string")
                end
                res[#res + 1] = parse_unicode_escape(hex)
                j = j + 5
            else
                if not escape_chars[c] then
                    decode_error(str, j, "invalid escape char '" .. c .. "' in string")
                end
                res[#res + 1] = escape_char_map_inv[c]
                j = j + 1
            end
            k = j
        elseif x == 34 then -- `"`: End of string
            res[#res + 1] = string_sub(str, k, j - 1)
            return table_concat(res), j + 1
        else
            j = j + 1
        end
    end

    decode_error(str, i, "expected closing quote for string")
end

local function parse_number(str, i)
    local x = next_char(str, i, delim_chars)
    local s = string_sub(str, i, x - 1)
    local n = tonumber(s)
    if not n then
        decode_error(str, i, "invalid number '" .. s .. "'")
    end
    return n, x
end

local function parse_literal(str, i)
    local x = next_char(str, i, delim_chars)
    local word = string_sub(str, i, x - 1)
    if not literals[word] then
        decode_error(str, i, "invalid literal '" .. word .. "'")
    end
    return literal_map[word], x
end

local function parse_array(str, i)
    local res = {}
    local n = 1
    i = i + 1
    while true do
        local x
        i = next_char(str, i, space_chars, true)
        -- Empty / end of array?
        if string_sub(str, i, i) == "]" then
            i = i + 1
            break
        end
        -- Read value
        x, i = parse(str, i)
        res[n] = x
        n = n + 1
        -- Next value
        i = next_char(str, i, space_chars, true)
        local chr = string_sub(str, i, i)
        if chr == "]" then
            i = i + 1
            break
        elseif chr ~= "," then
            decode_error(str, i, "expected ']' or ','")
        end
        i = i + 1
    end
    return res, i
end

local function parse_object(str, i)
    local res = {}
    i = i + 1
    while true do
        local key, val
        i = next_char(str, i, space_chars, true)
        -- Empty / end of object?
        if string_sub(str, i, i) == "}" then
            i = i + 1
            break
        end
        -- Read key
        if string_sub(str, i, i) ~= '"' then
            decode_error(str, i, "expected string for key")
        end
        key, i = parse_string(str, i)
        -- Read ":"
        i = next_char(str, i, space_chars, true)
        if string_sub(str, i, i) ~= ":" then
            decode_error(str, i, "expected ':' after key")
        end
        i = next_char(str, i + 1, space_chars, true)
        -- Read value
        val, i = parse(str, i)
        -- Set
        res[key] = val
        -- Next value
        i = next_char(str, i, space_chars, true)
        local chr = string_sub(str, i, i)
        if chr == "}" then
            i = i + 1
            break
        elseif chr ~= "," then
            decode_error(str, i, "expected '}' or ','")
        end
        i = i + 1
    end
    return res, i
end

local char_func_map = {
    ['"'] = parse_string,
    ["0"] = parse_number,
    ["1"] = parse_number,
    ["2"] = parse_number,
    ["3"] = parse_number,
    ["4"] = parse_number,
    ["5"] = parse_number,
    ["6"] = parse_number,
    ["7"] = parse_number,
    ["8"] = parse_number,
    ["9"] = parse_number,
    ["-"] = parse_number,
    ["t"] = parse_literal,
    ["f"] = parse_literal,
    ["n"] = parse_literal,
    ["["] = parse_array,
    ["{"] = parse_object,
}

parse = function(str, idx)
    local chr = string_sub(str, idx, idx)
    local f = char_func_map[chr]
    if f then
        return f(str, idx)
    end
    decode_error(str, idx, "unexpected character '" .. chr .. "'")
end

function json.decode(str)
    if type(str) ~= "string" then
        error("expected string, got " .. type(str))
    end
    local res, idx = parse(str, next_char(str, 1, space_chars, true))
    idx = next_char(str, idx, space_chars, true)
    if idx <= string_len(str) then
        decode_error(str, idx, "trailing garbage")
    end
    return res
end

-- -------------------------------------------------------------------------
-- Pretty Print / Utility Functions
-- -------------------------------------------------------------------------

--- Encode with pretty printing
--- @param val any Value to encode
--- @param indent string|nil Indent string (default "  ")
--- @return string JSON string
function json.encode_pretty(val, indent)
    indent = indent or "  "
    local function encode_pretty_impl(v, depth)
        local t = type(v)
        if t == "nil" then
            return "null"
        elseif t == "boolean" then
            return tostring(v)
        elseif t == "number" then
            return encode_number(v)
        elseif t == "string" then
            return encode_string(v)
        elseif t == "table" then
            local res = {}
            local is_array = true
            local max_index = 0
            local count = 0

            for k, _ in pairs(v) do
                count = count + 1
                if type(k) == "number" and k > 0 and math_floor(k) == k then
                    if k > max_index then max_index = k end
                else
                    is_array = false
                    break
                end
            end

            if count == 0 then
                return "{}"
            end

            local prefix = string.rep(indent, depth + 1)
            local close_prefix = string.rep(indent, depth)

            if is_array then
                for i = 1, max_index do
                    local item = rawget(v, i)
                    if item == nil then
                        res[#res + 1] = prefix .. "null"
                    else
                        res[#res + 1] = prefix .. encode_pretty_impl(item, depth + 1)
                    end
                end
                return "[\n" .. table_concat(res, ",\n") .. "\n" .. close_prefix .. "]"
            else
                local keys = {}
                for k in pairs(v) do
                    keys[#keys + 1] = tostring(k)
                end
                table_sort(keys)

                for i = 1, #keys do
                    local k = keys[i]
                    local item = v[k]
                    if item == nil then item = v[tonumber(k)] end
                    if item ~= nil then
                        local encoded_key = '"' .. string_gsub(k, '[%z\1-\31\\"]', escape_char) .. '"'
                        res[#res + 1] = prefix .. encoded_key .. ": " .. encode_pretty_impl(item, depth + 1)
                    end
                end
                return "{\n" .. table_concat(res, ",\n") .. "\n" .. close_prefix .. "}"
            end
        else
            error("unexpected type '" .. t .. "'")
        end
    end
    return encode_pretty_impl(val, 0)
end

function json.safe_decode(str)
    local ok, res = pcall(json.decode, str)
    return ok, res
end

return json
